package cc.shacocloud.mirage.restful.cors;

import cc.shacocloud.mirage.restful.util.HttpMethodUtils;
import cc.shacocloud.mirage.utils.charSequence.StrUtil;
import cc.shacocloud.mirage.utils.collection.CollUtil;
import io.vertx.core.http.HttpMethod;
import org.jetbrains.annotations.Nullable;

import java.time.Duration;
import java.util.*;
import java.util.stream.Collectors;

/**
 * 用于CORS配置的容器，以及用于检查给定请求的实际源、HTTP方法和头的方法。
 *
 * @author 思追(shaco)
 * @see <a href="https://www.w3.org/TR/cors/">CORS spec</a>
 */
public class CorsConfiguration {
    
    /**
     * 表示所有来源、方法或头的通配符。
     */
    public static final String ALL = "*";
    
    private static final List<HttpMethod> DEFAULT_METHODS = Collections.unmodifiableList(
            Arrays.asList(HttpMethod.GET, HttpMethod.HEAD));
    
    private static final List<String> DEFAULT_PERMIT_METHODS = Collections.unmodifiableList(
            Arrays.asList(HttpMethod.GET.name(), HttpMethod.HEAD.name(), HttpMethod.POST.name()));
    
    private static final List<String> DEFAULT_PERMIT_ALL = Collections.unmodifiableList(
            Collections.singletonList(ALL));
    
    @Nullable
    private List<String> allowedOrigins;
    
    @Nullable
    private List<String> allowedMethods;
    
    @Nullable
    private List<HttpMethod> resolvedMethods = DEFAULT_METHODS;
    
    @Nullable
    private List<String> allowedHeaders;
    
    @Nullable
    private List<String> exposedHeaders;
    
    @Nullable
    private Boolean allowCredentials;
    
    @Nullable
    private Long maxAge;
    
    
    /**
     * 构造一个新的{@code CorsConfiguration}实例，默认情况下不允许对任何源进行跨源请求。
     *
     * @see #applyPermitDefaultValues()
     */
    public CorsConfiguration() {
    }
    
    /**
     * 通过从提供的{@code CorsConfiguration}中复制所有值来构造一个新的{@code CorsConfiguration}实例。
     */
    public CorsConfiguration(CorsConfiguration other) {
        this.allowedOrigins = other.allowedOrigins;
        this.allowedMethods = other.allowedMethods;
        this.resolvedMethods = other.resolvedMethods;
        this.allowedHeaders = other.allowedHeaders;
        this.exposedHeaders = other.exposedHeaders;
        this.allowCredentials = other.allowCredentials;
        this.maxAge = other.maxAge;
    }
    
    /**
     * 返回配置的源文件以允许，如果没有，则返回{@code null}
     *
     * @see #addAllowedOrigin(String)
     * @see #setAllowedOrigins(List)
     */
    @Nullable
    public List<String> getAllowedOrigins() {
        return this.allowedOrigins;
    }
    
    /**
     * 设置源允许，例如{@code "https://domain1.com"}
     * <p>特殊值{@code "*"}允许所有域。
     * <p>默认情况下没有设置
     */
    public void setAllowedOrigins(@Nullable List<String> allowedOrigins) {
        this.allowedOrigins = (allowedOrigins != null ? new ArrayList<>(allowedOrigins) : null);
    }
    
    /**
     * 添加一个允许的来源
     */
    public void addAllowedOrigin(String origin) {
        if (this.allowedOrigins == null) {
            this.allowedOrigins = new ArrayList<>(4);
        } else if (this.allowedOrigins == DEFAULT_PERMIT_ALL) {
            setAllowedOrigins(DEFAULT_PERMIT_ALL);
        }
        this.allowedOrigins.add(origin);
    }
    
    /**
     * 返回允许的HTTP方法，或{@code null}，这种情况下只允许{@code GET}和{@code HEAD}。
     *
     * @see #addAllowedMethod(HttpMethod)
     * @see #addAllowedMethod(String)
     * @see #setAllowedMethods(List)
     */
    @Nullable
    public List<String> getAllowedMethods() {
        return this.allowedMethods;
    }
    
    /**
     * 设置允许的HTTP方法，例如 {@code GET}，{@code POST}， {@code "PUT"}
     *
     * <p>特殊值{@code "*"}允许所有方法。
     * <p>如果未设置，则只允许{@code GET}和{@code HEAD}。
     * <p>默认情况下没有设置。
     */
    public void setAllowedMethods(@Nullable List<String> allowedMethods) {
        this.allowedMethods = (allowedMethods != null ? new ArrayList<>(allowedMethods) : null);
        if (CollUtil.isNotEmpty(allowedMethods)) {
            this.resolvedMethods = new ArrayList<>(allowedMethods.size());
            for (String method : allowedMethods) {
                if (ALL.equals(method)) {
                    this.resolvedMethods = null;
                    break;
                }
                this.resolvedMethods.add(HttpMethodUtils.resolve(method));
            }
        } else {
            this.resolvedMethods = DEFAULT_METHODS;
        }
    }
    
    /**
     * 添加一个HTTP方法来允许
     */
    public void addAllowedMethod(HttpMethod method) {
        addAllowedMethod(method.name());
    }
    
    /**
     * 添加一个HTTP方法来允许。
     */
    public void addAllowedMethod(String method) {
        if (StrUtil.isNotBlank(method)) {
            if (this.allowedMethods == null) {
                this.allowedMethods = new ArrayList<>(4);
                this.resolvedMethods = new ArrayList<>(4);
            } else if (this.allowedMethods == DEFAULT_PERMIT_METHODS) {
                setAllowedMethods(DEFAULT_PERMIT_METHODS);
            }
            this.allowedMethods.add(method);
            if (ALL.equals(method)) {
                this.resolvedMethods = null;
            } else if (this.resolvedMethods != null) {
                this.resolvedMethods.add(HttpMethodUtils.resolve(method));
            }
        }
    }
    
    /**
     * 返回允许的实际请求头，如果没有，返回{@code null}。
     *
     * @see #addAllowedHeader(String)
     * @see #setAllowedHeaders(List)
     */
    @Nullable
    public List<String> getAllowedHeaders() {
        return this.allowedHeaders;
    }
    
    /**
     * 设置在实际请求期间允许使用的飞行前请求可以列出的头的列表。
     * <p>特殊值{@code "*"}允许实际请求发送任何头部。
     * <p>不需要设置请求头，
     * {@code Cache-Control}, {@code Content-Language}, {@code Expires},
     * {@code Last-Modified}, or {@code Pragma}.
     * <p>默认情况下没有设置
     */
    public void setAllowedHeaders(@Nullable List<String> allowedHeaders) {
        this.allowedHeaders = (allowedHeaders != null ? new ArrayList<>(allowedHeaders) : null);
    }
    
    /**
     * 添加一个实际的请求头来允许。
     */
    public void addAllowedHeader(String allowedHeader) {
        if (this.allowedHeaders == null) {
            this.allowedHeaders = new ArrayList<>(4);
        } else if (this.allowedHeaders == DEFAULT_PERMIT_ALL) {
            setAllowedHeaders(DEFAULT_PERMIT_ALL);
        }
        this.allowedHeaders.add(allowedHeader);
    }
    
    /**
     * 返回要公开的配置响应标头，如果没有，则返回{@code null}。
     *
     * @see #addExposedHeader(String)
     * @see #setExposedHeaders(List)
     */
    @Nullable
    public List<String> getExposedHeaders() {
        return this.exposedHeaders;
    }
    
    /**
     * 设置响应头的列表，而不是简单的头，例如：
     * {@code Cache-Control}, {@code Content-Language}, {@code Content-Type},
     * {@code Expires}, {@code Last-Modified}, {@code Pragma})
     * <p>注意 {@code "*"} 不是有效的值。
     * <p>默认情况下没有设置。
     */
    public void setExposedHeaders(@Nullable List<String> exposedHeaders) {
        if (exposedHeaders != null && exposedHeaders.contains(ALL)) {
            throw new IllegalArgumentException("'*' 不是一个有效的值");
        }
        this.exposedHeaders = (exposedHeaders != null ? new ArrayList<>(exposedHeaders) : null);
    }
    
    /**
     * 添加要公开的响应标头。
     * <p>注意{@code "*"} 不是有效的值。
     */
    public void addExposedHeader(String exposedHeader) {
        if (ALL.equals(exposedHeader)) {
            throw new IllegalArgumentException("'*' 不是一个有效的值");
        }
        if (this.exposedHeaders == null) {
            this.exposedHeaders = new ArrayList<>(4);
        }
        this.exposedHeaders.add(exposedHeader);
    }
    
    /**
     * 返回配置的{@code allowCredentials}标志，如果没有，返回{@code null}。
     *
     * @see #setAllowCredentials(Boolean)
     */
    @Nullable
    public Boolean getAllowCredentials() {
        return this.allowCredentials;
    }
    
    /**
     * 是否支持用户凭证。
     * <p>默认情况下，这是没有设置的(即不支持用户凭证)。
     */
    public void setAllowCredentials(@Nullable Boolean allowCredentials) {
        this.allowCredentials = allowCredentials;
    }
    
    /**
     * 返回配置的{@code maxAge}值，如果没有，则返回{@code null}。
     *
     * @see #setMaxAge(Long)
     */
    @Nullable
    public Long getMaxAge() {
        return this.maxAge;
    }
    
    /**
     * 配置时长，作为持续时间
     *
     * @see #setMaxAge(Long)
     */
    public void setMaxAge(Duration maxAge) {
        this.maxAge = maxAge.getSeconds();
    }
    
    /**
     * 配置客户机可以缓存飞行前请求的响应的时间(以秒为单位)。
     * <p>默认情况下没有设置。
     */
    public void setMaxAge(@Nullable Long maxAge) {
        this.maxAge = maxAge;
    }
    
    /**
     * 默认情况下，新创建的CorsConfiguration不允许任何跨源请求，必须显式配置以指示应该允许哪些请求。
     * <p>
     * 使用此方法翻转初始化模型，使其从开放默认值开始，允许对GET、HEAD和POST请求的所有跨源请求。
     * 但是请注意，此方法不会覆盖已经设置的任何现有值
     * <p>
     * 如果尚未设置，则应用以下默认值:
     * <ul>
     *  <li>允许所有的起源。</li>
     *  <li>允许简单方法 {@code GET}， {@code HEAD}和{@code POST}</li>
     *  <li>允许所有头</li>
     *  <li>最大时长设置为1800秒(30分钟)。</li>
     * </ul>
     */
    public CorsConfiguration applyPermitDefaultValues() {
        if (this.allowedOrigins == null) {
            this.allowedOrigins = DEFAULT_PERMIT_ALL;
        }
        if (this.allowedMethods == null) {
            this.allowedMethods = DEFAULT_PERMIT_METHODS;
            this.resolvedMethods = DEFAULT_PERMIT_METHODS
                    .stream().map(HttpMethodUtils::resolve).collect(Collectors.toList());
        }
        if (this.allowedHeaders == null) {
            this.allowedHeaders = DEFAULT_PERMIT_ALL;
        }
        if (this.maxAge == null) {
            this.maxAge = 1800L;
        }
        return this;
    }
    
    /**
     * 将提供的{@code CorsConfiguration}的非空属性与此属性组合在一起。
     * <p>
     * 当组合单个值(如{@code allowCredentials}或{@code maxAge})时，这些属性会被其他非空属性(如果有的话)覆盖
     *
     * <p>
     * 组合像{@code allowedOrigins}， {@code allowedMethods}这样的列表， {@code allowedHeaders}或{@code exposedHeaders}是以附加的方式完成的。
     * 例如：
     * 将{@code [GET，POST]}和{@code [PATCH]}组合得到{@code [GET，POST，PATCH]}，
     * 但是请注意，将{@code [GET，POST}和{@code [*]组合得到的结果是 {@code [*]}。
     * <p>
     * 注意，由{@link CorsConfiguration#applyPermitDefaultValues()}设置的默认许可值会被显式定义的任何值覆盖。
     *
     * @return 组合的{@code CorsConfiguration}或{@code this}配置(如果提供的配置是{@code null})
     */
    @Nullable
    public CorsConfiguration combine(@Nullable CorsConfiguration other) {
        if (other == null) {
            return this;
        }
        CorsConfiguration config = new CorsConfiguration(this);
        config.setAllowedOrigins(combine(getAllowedOrigins(), other.getAllowedOrigins()));
        config.setAllowedMethods(combine(getAllowedMethods(), other.getAllowedMethods()));
        config.setAllowedHeaders(combine(getAllowedHeaders(), other.getAllowedHeaders()));
        config.setExposedHeaders(combine(getExposedHeaders(), other.getExposedHeaders()));
        Boolean allowCredentials = other.getAllowCredentials();
        if (allowCredentials != null) {
            config.setAllowCredentials(allowCredentials);
        }
        Long maxAge = other.getMaxAge();
        if (maxAge != null) {
            config.setMaxAge(maxAge);
        }
        return config;
    }
    
    private List<String> combine(@Nullable List<String> source, @Nullable List<String> other) {
        if (other == null) {
            return (source != null ? source : Collections.emptyList());
        }
        if (source == null) {
            return other;
        }
        if (source == DEFAULT_PERMIT_ALL || source == DEFAULT_PERMIT_METHODS) {
            return other;
        }
        if (other == DEFAULT_PERMIT_ALL || other == DEFAULT_PERMIT_METHODS) {
            return source;
        }
        if (source.contains(ALL) || other.contains(ALL)) {
            return new ArrayList<>(Collections.singletonList(ALL));
        }
        Set<String> combined = new LinkedHashSet<>(source);
        combined.addAll(other);
        return new ArrayList<>(combined);
    }
    
    /**
     * 根据配置的允许的起始点检查请求的起始点。
     *
     * @param requestOrigin 要检查的原点
     * @return 用于响应的原点，或{@code null}，这意味着不允许请求原点
     */
    @Nullable
    public String checkOrigin(@Nullable String requestOrigin) {
        if (StrUtil.isBlank(requestOrigin)) {
            return null;
        }
        if (CollUtil.isEmpty(this.allowedOrigins)) {
            return null;
        }
        
        if (this.allowedOrigins.contains(ALL)) {
            if (this.allowCredentials != Boolean.TRUE) {
                return ALL;
            } else {
                return requestOrigin;
            }
        }
        for (String allowedOrigin : this.allowedOrigins) {
            if (requestOrigin.equalsIgnoreCase(allowedOrigin)) {
                return requestOrigin;
            }
        }
        
        return null;
    }
    
    /**
     * 检查HTTP请求方法(请求前的{@code Access-Control-Request-Method}头中的方法)和已配置允许的方法。
     *
     * @param requestMethod 要检查的HTTP请求方法
     * @return 要在请求前的响应中列出的HTTP方法列表，或者如果不允许提供的{@code requestMethod}，则为{@code null}
     */
    @Nullable
    public List<HttpMethod> checkHttpMethod(@Nullable HttpMethod requestMethod) {
        if (requestMethod == null) {
            return null;
        }
        if (this.resolvedMethods == null) {
            return Collections.singletonList(requestMethod);
        }
        return (this.resolvedMethods.contains(requestMethod) ? this.resolvedMethods : null);
    }
    
    /**
     * 检查提供的请求标头(请求前的{@code Access-Control-Request-Headers}中列出的标头)与配置允许的标头。
     *
     * @param requestHeaders 要检查的请求标头
     * @return 在请求前的响应中列出的允许的头的列表，如果提供的请求头都不允许，则为{@code null}
     */
    @Nullable
    public List<String> checkHeaders(@Nullable List<String> requestHeaders) {
        if (requestHeaders == null) {
            return null;
        }
        if (requestHeaders.isEmpty()) {
            return Collections.emptyList();
        }
        if (CollUtil.isEmpty(this.allowedHeaders)) {
            return null;
        }
        
        boolean allowAnyHeader = this.allowedHeaders.contains(ALL);
        List<String> result = new ArrayList<>(requestHeaders.size());
        for (String requestHeader : requestHeaders) {
            if (StrUtil.isNotBlank(requestHeader)) {
                requestHeader = requestHeader.trim();
                if (allowAnyHeader) {
                    result.add(requestHeader);
                } else {
                    for (String allowedHeader : this.allowedHeaders) {
                        if (requestHeader.equalsIgnoreCase(allowedHeader)) {
                            result.add(requestHeader);
                            break;
                        }
                    }
                }
            }
        }
        return (result.isEmpty() ? null : result);
    }
    
}
